iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 21
0
Mobile Development

RxSwift / 30天探索之旅系列 第 21

第 21 天 - TableView + Rx 與範例(下)

  • 分享至 

  • xImage
  •  

嗨嗨,接續昨天範例的部分,今天繼續往下走,介紹ViewModel的部分,我想不管是MVVMC或是MVVM,ViewModel都不擇了資料跟邏輯的部分,ViewController負責邏輯的部分,所以在兩者之間,使用Rx來做binding。

ViewModel

以Pure Function概念出發,我們也可以把ViewModel分為Input跟Output,Input就是會進到邏輯處理資料或事件;Output就是可能會給Controller接收或是Coordinator接收的Observable。

宣告變數部分可以分為Input跟Output

// MARK: - Input
let apiService: ProductAPIProtocol
let triggerAPI: AnyObserver<Void>
let triggerNextPage: AnyObserver<Void>

// MARK: - Output
let didClose: Observable<FinishReason>
let isLoading: Observable<Bool>
let data: Observable<[Product]>
let page: Observable<Int>
let showError: Observable<Error>

// MARK: - Private
private let disposeBag = DisposeBag()

Input的部分

  1. triggerAPI來接收reload事件
  2. triggerNextPage則是用來接收下一頁事件

Output的部分

  1. didClose是來發送這個頁面或是說這模組的關閉原因
  2. isLoading是發送現在是否要呈現Activity indicator
  3. data是發送從目前呈現在頁面上的資料
  4. showError是包含所以可能發生錯誤的事件

Constructor部分就會包含變數的定義,

// MARK: - Constructor
init(apiService: ProductAPIProtocol) {
    self.apiService = apiService

    didClose = .never()
  1. 建構時,帶入apiService作為參數,apiService定義了這整個功能所需要call API,用注入的方式是為了之後再測試時,可以mock測試的API
  2. didClose是這個模組離開或結束的原因,可能因為使用者取消,或使用者選了一個選項...等原因,這模組結束,而需要轉跳到別頁。但因為目前做的這功能就是APP開啟的第一個模組,沒有要dimiss的需求,先放.never()
let pageRelay = BehaviorRelay<Int>(value: 1)
page = pageRelay.asObservable()

let productListRelay = BehaviorRelay<[Product]>(value: [])
data = productListRelay.asObservable()

let indicator = ActivityIndicator()
isLoading = indicator.asObservable()

let triggerAPISubject = PublishSubject<Void>()
triggerAPI = triggerAPISubject.asObserver()

let triggerNextPageSubject = PublishSubject<Void>()
triggerNextPage = triggerNextPageSubject.asObserver()
  1. pageRelayproductListRelay分別是目前頁面與產品的資料,使用Relay是因為在串接過程中要加減一頁,或是新增或清空陣列,用Relay很方便。
  2. 但我們在第 8 天 - 淺談Subject使用時機 - iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天有聊到要謹慎使用Subject,所以,對viewModel以外來說,它並沒有需要使用到它們,因為我對外只提供data這Observable,對外只能訂閱,並不能修改。
  3. triggerAPISubjecttriggerNextPageSubject作為中介者,對外開放Observer接收到訊息,接收到之後,對內作為Observable發送給訂閱者,這對應到第 5 天 - Subject (上) - iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天所講的Subject特性,但同樣的,為了避免被到處.onNext(),這裡只對外開放Observer。
let fetchProductListResult = triggerAPISubject.asObservable().startWith(())
    .flatMapLatest { _ in apiService.request(page: 1).trackActivity(indicator).materialize() }
    .share()

fetchProductListResult.elements()
    .bind { products -> Void in
        pageRelay.accept(1)
        productListRelay.accept(products)
    }
    .disposed(by: disposeBag)
  1. fetchProductListResult是接收triggerAPISubject.next事件後,去call API,並回傳回來的資料,這對應到第 11 天 - Transforming Observables(下) - iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天所說的,因為一進畫面需要先載入一次資料,所以用startWith先觸發一次
  2. fetchProductListResult.elements()這整段則是在處理改變pageRelay的數值為1,以及將productListRelay覆蓋為新的資料。
let nextProductListResult = triggerNextPageSubject.asObservable()
    .debounce(.seconds(2), scheduler: MainScheduler.instance)
    .flatMapLatest { _ in apiService.request(page: pageRelay.value+1).trackActivity(indicator).materialize() }
    .share()

nextProductListResult.elements()
    .bind { products -> Void in
        let newPage = pageRelay.value + 1
        let newData = productListRelay.value + products

        pageRelay.accept(newPage)
        productListRelay.accept(newData)
    }
    .disposed(by: disposeBag)}
  1. nextProductListResult與fetchProductListResult類似,考慮到下拉到tableView最底時,還是繼續一直滾,這樣可能會頻繁發送.next(Void),這不是我們預期的,所以這裡放置debounce來過濾掉頻繁的元素
  2. nextProductListResult.elements()這段仍是處理pageRelayproductListRelay,我們將數值先暫存,更新後再存放回去。
showError = Observable.merge(fetchProductListResult.errors(),
                             nextProductListResult.errors())

最後將所以可能會發生錯誤的地方,匯集到showError,在由要呈現錯誤訊息的地方去訂閱。


目前這樣寫雖然一看就懂,但缺點就是有點冗,總感覺nextPage跟reload是可以合併在一起,礙於道行還不夠,或許有大大經過可以提點一下,又或是我三個月後再來看,又會有新的感覺吧!明天講講測試,RxTest跟RxBlocking,掰掰


上一篇
第 20 天 - TableView + Rx 與範例(上)
下一篇
第 22 天 - RxBlocking & RxTest
系列文
RxSwift / 30天探索之旅30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言